/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.juli;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

/**
 * <p>Cache structure for SimpleDateFormat formatted timestamps based on
 * seconds.</p>
 * <p>
 * <p>Millisecond formatting using S is not supported. You should add the
 * millisecond information after getting back the second formatting.</p>
 * <p>
 * <p>The cache consists of entries for a consecutive range of
 * seconds. The length of the range is configurable. It is
 * implemented based on a cyclic buffer. New entries shift the range.</p>
 * <p>
 * <p>The cache is not threadsafe. It can be used without synchronization
 * via thread local instances, or with synchronization as a global cache.</p>
 * <p>
 * <p>The cache can be created with a parent cache to build a cache hierarchy.
 * Access to the parent cache is threadsafe.</p>
 */
public class DateFormatCache {

	private static final String msecPattern = "#";

	/* Timestamp format */
	private final String format;

	/* Number of cached entries */
	private int cacheSize = 0;

	private Cache cache;

	public DateFormatCache(int size, String format, DateFormatCache parent) {
		cacheSize = size;
		this.format = tidyFormat(format);
		Cache parentCache = null;
		if (parent != null) {
			synchronized (parent) {
				parentCache = parent.cache;
			}
		}
		cache = new Cache(parentCache);
	}

	/**
	 * Replace the millisecond formatting character 'S' by
	 * some dummy characters in order to make the resulting
	 * formatted time stamps cacheable. Our consumer might
	 * choose to replace the dummy chars with the actual
	 * milliseconds because that's relatively cheap.
	 */
	private String tidyFormat(String format) {
		boolean escape = false;
		StringBuilder result = new StringBuilder();
		int len = format.length();
		char x;
		for (int i = 0; i < len; i++) {
			x = format.charAt(i);
			if (escape || x != 'S') {
				result.append(x);
			} else {
				result.append(msecPattern);
			}
			if (x == '\'') {
				escape = !escape;
			}
		}
		return result.toString();
	}

	public String getFormat(long time) {
		return cache.getFormat(time);
	}

	private class Cache {

		/* Helper object to be able to call SimpleDateFormat.format(). */
		private final Date currentDate = new Date();
		/* Second formatted in most recent invocation */
		private long previousSeconds = Long.MIN_VALUE;
		/* Formatted timestamp generated in most recent invocation */
		private String previousFormat = "";
		/* First second contained in cache */
		private long first = Long.MIN_VALUE;
		/* Last second contained in cache */
		private long last = Long.MIN_VALUE;
		/* Index of "first" in the cyclic cache */
		private int offset = 0;
		private String cache[];
		private SimpleDateFormat formatter;

		private Cache parent = null;

		private Cache(Cache parent) {
			cache = new String[cacheSize];
			for (int i = 0; i < cacheSize; i++) {
				cache[i] = null;
			}
			formatter = new SimpleDateFormat(format, Locale.US);
			formatter.setTimeZone(TimeZone.getDefault());
			this.parent = parent;
		}

		private String getFormat(long time) {

			long seconds = time / 1000;

            /* First step: if we have seen this timestamp
               during the previous call, return the previous value. */
			if (seconds == previousSeconds) {
				return previousFormat;
			}

            /* Second step: Try to locate in cache */
			previousSeconds = seconds;
			int index = (offset + (int) (seconds - first)) % cacheSize;
			if (index < 0) {
				index += cacheSize;
			}
			if (seconds >= first && seconds <= last) {
				if (cache[index] != null) {
	                /* Found, so remember for next call and return.*/
					previousFormat = cache[index];
					return previousFormat;
				}

            /* Third step: not found in cache, adjust cache and add item */
			} else if (seconds >= last + cacheSize || seconds <= first - cacheSize) {
				first = seconds;
				last = first + cacheSize - 1;
				index = 0;
				offset = 0;
				for (int i = 1; i < cacheSize; i++) {
					cache[i] = null;
				}
			} else if (seconds > last) {
				for (int i = 1; i < seconds - last; i++) {
					cache[(index + cacheSize - i) % cacheSize] = null;
				}
				first = seconds - (cacheSize - 1);
				last = seconds;
				offset = (index + 1) % cacheSize;
			} else if (seconds < first) {
				for (int i = 1; i < first - seconds; i++) {
					cache[(index + i) % cacheSize] = null;
				}
				first = seconds;
				last = seconds + (cacheSize - 1);
				offset = index;
			}

            /* Last step: format new timestamp either using
             * parent cache or locally. */
			if (parent != null) {
				synchronized (parent) {
					previousFormat = parent.getFormat(time);
				}
			} else {
				currentDate.setTime(time);
				previousFormat = formatter.format(currentDate);
			}
			cache[index] = previousFormat;
			return previousFormat;
		}
	}
}
